package com.yeskery.nut.script.parser;

import com.yeskery.nut.script.Metadata;
import com.yeskery.nut.script.expression.*;

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 表达式函数的脚本元数据转换器
 * @author sprout
 * 2022-05-14 12:40
 */
public class ExpressionScriptMetadataParser extends TextScriptMetadataParser {

    /** 表达式符号 */
    public static final String EXPRESSION_SYMBOL = "%";

    /** 表达式开始符号 */
    public static final char EXPRESSION_START_SYMBOL = '(';

    /** 表达式结束符号 */
    public static final char EXPRESSION_END_SYMBOL = ')';

    /** 支持的所有的表达式 */
    private final Map<String, NutExpression> expressions = new ConcurrentHashMap<>();

    /**
     * 构建表达式函数的脚本元数据转换器
     * @param customNutExpressions 自定义视图表达式
     */
    public ExpressionScriptMetadataParser(List<NutExpression> customNutExpressions) {
        this();
        customNutExpressions.forEach(this::registerExpression);
    }

    /**
     * 构建表达式函数的脚本元数据转换器
     */
    public ExpressionScriptMetadataParser() {
        registerExpression(new TextExpression());
        registerExpression(new HtmlExpression());
        registerExpression(new XmlExpression());
        registerExpression(new JsonExpression());
        registerExpression(new FileExpression());
        registerExpression(new ImageExpression());
    }

    /**
     * 注册表达式
     * @param expression 表达式
     */
    public void registerExpression(NutExpression expression) {
        if (expression == null) {
            throw new NullPointerException();
        }
        expressions.put(expression.getName(), expression);
    }

    @Override
    public Metadata parseMetadata(String script) {
        Metadata metadata = super.parseMetadata(script);
        if (metadata == Metadata.EMPTY_METADATA) {
            return metadata;
        }
        ResponseMetadata responseMetadata = ResponseMetadata.fromMetadata(metadata);
        String body = metadata.getBody();
        String functionBody = getFirstLine(body);
        Stack<NutExpressionIndex> expressionStack = getExpressionStack(body, functionBody);
        boolean firstPop = true;
        NutExpressionIndex lastExpressionIndex = null;
        while (!expressionStack.empty()) {
            byte[] content;
            NutExpressionIndex expressionIndex = expressionStack.pop();
            if (firstPop) {
                int startIndex = expressionIndex.index;
                int endIndex = body.lastIndexOf(EXPRESSION_END_SYMBOL, body.length() - 1 - expressionStack.size());
                if (endIndex == -1 || startIndex > endIndex) {
                    throw new ScriptParseException("Invalid Script Expression Function At Expression: [" + script + "]");
                }
                content = body.substring(startIndex, endIndex).getBytes(StandardCharsets.UTF_8);
                firstPop = false;
            } else {
                content = lastExpressionIndex.content;
            }
            expressionIndex.content = expressionIndex.nutExpression.handle(new String(content, StandardCharsets.UTF_8));
            lastExpressionIndex = expressionIndex;
        }
        if (lastExpressionIndex != null) {
            responseMetadata.setMediaType(lastExpressionIndex.nutExpression.getMediaType());
            responseMetadata.setResponse(lastExpressionIndex.content);
            responseMetadata.setBody(new String(responseMetadata.getResponse(), StandardCharsets.UTF_8));
        } else {
            if (responseMetadata.getResponse() == null) {
                responseMetadata.setResponse(body.getBytes(StandardCharsets.UTF_8));
            }
        }
        return responseMetadata;
    }

    @Override
    protected String getMultiFirstLine(String value) {
        if (value.startsWith(EXPRESSION_SYMBOL)) {
            int index = value.indexOf(MULTI_LINE_SYMBOL);
            if (index != -1) {
                return value.substring(0, index) + super.getMultiFirstLine(index >= value.length() - 1 ? "" : value.substring(index + 1));
            }
        }
        return super.getMultiFirstLine(value);
    }

    /**
     * 获取表达式栈
     * @param value 表达式
     * @return 表达式栈
     */
    private Stack<NutExpressionIndex> getExpressionStack(String script, String value) {
        Stack<NutExpressionIndex> stack = new Stack<>();
        int i, k = 0;
        String tempValue = value;
        while ((i = tempValue.indexOf(EXPRESSION_START_SYMBOL)) != -1) {
            k = k + i;
            int j = tempValue.lastIndexOf(EXPRESSION_SYMBOL, i);
            if (j == -1 || j >= tempValue.length() - 1) {
                break;
            }
            String expressionName = tempValue.substring(j + 1, i);
            NutExpression nutExpression = expressions.get(expressionName);
            if (nutExpression == null) {
                throw new ScriptParseException("Unsupported Script Expression: [" + expressionName + "] At Expression: [" + value + "]");
            }
            if (i >= tempValue.length() - 1) {
                throw new ScriptParseException("Invalid Script Expression Body [" + expressionName + "] At Expression: [" + value + "]");
            }
            stack.push(new NutExpressionIndex(nutExpression, script.indexOf(value) + k + 1));
            tempValue = tempValue.substring(i + 1);
            k++;
        }
        return stack;
    }

    /**
     * 获取第一行字符串
     * @param value 字符串
     * @return 第一行字符串
     */
    private String getFirstLine(String value) {
        return value.split(String.valueOf(NEW_LINE_SEPARATOR))[0];
    }

    /**
     * 函数表达式索引对象
     */
    private static class NutExpressionIndex {
        /** 表达式函数 */
        private final NutExpression nutExpression;
        /** 索引 */
        private final int index;
        /** 表达式内容 */
        private byte[] content;

        /**
         * 构造函数表达式索引对象
         * @param nutExpression 表达式函数
         * @param index 索引
         */
        public NutExpressionIndex(NutExpression nutExpression, int index) {
            this.nutExpression = nutExpression;
            this.index = index;
        }
    }
}
